<?php


namespace XinSheng;

use RuntimeException;
use Fize\Math\Financial;

/**
 * 根据内部IRR推算外部年化利率
 */
class OutsideRateByIrr
{
    /**
     * @var float 转让价款
     */
    protected $leaseAmount;

    /**
     * @var float 转让价款比例
     */
    protected $leaseAmountRatio;

    /**
     * @var float 首付款
     */
    protected $downPayment;

    /**
     * @var float 首付款比例
     */
    protected $downPaymentRatio;

    /**
     * @var float 预付款
     */
    protected $deposit;

    /**
     * @var float 预付款比例
     */
    protected $depositRatio;

    /**
     * @var float 留购价
     */
    protected $residualValue;

    /**
     * @var float 留购价比例
     */
    protected $residualValueRatio;

    /**
     * @var float 手续费
     */
    protected $serviceAmount;

    /**
     * @var float 手续费比例
     */
    protected $serviceAmountRatio;

    /**
     * @var float 内部年化IRR
     */
    protected $interestRate;

    /**
     * @var float 内部年化IRR2
     */
    protected $interestRate2;

    /**
     * @var int 期次
     */
    protected $issues;

    /**
     * @var array 其他费用
     */
    protected $otherfees;

    /**
     * @var array 其他费用2
     */
    protected $otherfees2;

    /**
     * @var float|null 首次利息本金比
     */
    protected $interestRateFirst;

    /**
     * @var int 每期月数
     */
    protected $issueMonths;

    /**
     * @var int 支付方式：0-期末支付；1-期初支付
     */
    protected $paymentMode;

    /**
     * @var float 每期利率
     */
    protected $issueRate;

    /**
     * @var float 每期利率2
     */
    protected $issueRate2;

    /**
     * @var int 保留小数位
     */
    protected $scale;

    /**
     * @var array 前端计划表
     */
    protected $frontPlan;

    /**
     * @var float 外部年化IRR
     */
    protected $outsideRate;

    /**
     * 初始化
     * @param float $leaseAmount        转让价款
     * @param float $leaseAmountRatio   转让价款比例
     * @param float $downPayment        首付款
     * @param float $downPaymentRatio   首付款比例
     * @param float $deposit            预付款
     * @param float $depositRatio       预付款比例
     * @param float $residualValue      留购价
     * @param float $residualValueRatio 留购价比例
     * @param float $serviceAmount      手续费
     * @param float $serviceAmountRatio 手续费比例
     * @param float $interestRate2      内部年化IRR2
     * @param int   $issues             期次
     * @param array $otherfees2         其他费用
     * @param int   $paymentMode        支付方式：0-期末支付；1-期初支付
     * @param int   $issueMonths        每期月数
     * @param int   $scale              保留小数位
     */
    public function __construct(
        $leaseAmount, $leaseAmountRatio, $downPayment, $downPaymentRatio, $deposit,
        $depositRatio, $residualValue, $residualValueRatio, $serviceAmount, $serviceAmountRatio,
        $interestRate2, $issues, $otherfees2 = [], $paymentMode = 0, $issueMonths = 1,
        $scale = 2)
    {
        $this->leaseAmount = $leaseAmount;
        $this->leaseAmountRatio = $leaseAmountRatio;
        $this->downPayment = $downPayment;
        $this->downPaymentRatio = $downPaymentRatio;
        $this->deposit = $deposit;
        $this->depositRatio = $depositRatio;
        $this->residualValue = $residualValue;
        $this->residualValueRatio = $residualValueRatio;
        $this->serviceAmount = $serviceAmount;
        $this->serviceAmountRatio = $serviceAmountRatio;
        $this->interestRate2 = $interestRate2;
        $this->issues = $issues;
        $this->otherfees2 = $otherfees2;
        $this->paymentMode = $paymentMode;
        $this->issueMonths = $issueMonths;

        $this->issueRate2 = $this->interestRate2 * $this->issueMonths / 12;
        $this->scale = $scale;
    }

    /**
     * 设置前端
     * @param float      $interestRate      内部年化IRR
     * @param array      $otherfees         其他费用
     * @param float|null $interestRateFirst 首次利息本金比，null表示不指定
     */
    public function setFront($interestRate, $otherfees = [], $interestRateFirst = null)
    {
        $this->interestRate = $interestRate;
        $this->otherfees = $otherfees;
        $this->interestRateFirst = $interestRateFirst;

        $this->issueRate = $this->interestRate * $this->issueMonths / 12;
    }

    /**
     * 获取外部年化利率
     * @param int $maxiter 尝试次数
     * @return float
     */
    public function getOutsideRate($maxiter = 10000)
    {
        if (!is_null($this->outsideRate)) {
            return $this->outsideRate;
        }

        $guess = $this->interestRate2;
        $last_add_Guess = $guess;
        $last_sub_Guess = $guess;
        $residual = 1;
        $step = 0.01;
        $precision = 8;  # 精度，小数点后8位
        $epsMax = pow(10, -$precision);  # 误差
        while (abs($residual) > $epsMax && $maxiter > 0) {
            $maxiter -= 1;
            $cashflow = $this->getCashFlow($guess);
            $irr = Financial::irr($cashflow, $this->issueRate2);
            $rate = $irr * 12 / $this->issueMonths;
            $residual = $this->interestRate2 - $rate;
            if (abs($residual) > $epsMax) {
                if ($residual > 0) {
                    $guess += $step;
                    $last_add_Guess = $guess;
                } else {
                    $guess -= $step;
                    $last_sub_Guess = $guess;
                }
                $step = abs($last_add_Guess - $last_sub_Guess) / 2;
                if ($step == 0) {  // 精度丢失导致两猜测值已相同
                    break;
                }
            }
        }
        if ($step != 0 && abs($residual) > $epsMax) {
            throw new RuntimeException('测算次数过多！');
        }
        return $guess;
    }

    /**
     * 获取前端计划表
     * @return array
     */
    public function getFrontPlan()
    {
        if (is_null($this->frontPlan)) {
            $mbi = new ModelByIrr(
                $this->leaseAmount, $this->downPayment, $this->deposit, $this->residualValue, $this->serviceAmount,
                $this->interestRate, $this->issues, $this->otherfees, $this->interestRateFirst, $this->paymentMode,
                $this->issueMonths, $this->scale
            );
            $this->frontPlan = $mbi->getPlan();
        }
        return $this->frontPlan;
    }

    /**
     * 设置前端计划表
     * @param array $front_plan
     */
    public function setFrontPlan($front_plan)
    {
        $this->frontPlan = $front_plan;
    }

    /**
     * 获取现金流
     * @param float $outsideRate 外部年化利率
     */
    protected function getCashFlow($outsideRate)
    {
        $issueOutsideRate = $outsideRate * $this->issueMonths / 12;
        $cashflow = [];
        $balance = round($this->leaseAmount * $this->leaseAmountRatio - $this->downPayment * $this->downPaymentRatio, $this->scale);
        foreach ($this->getFrontPlan() as $issue => $front_plan) {
            if ($issue == 0) {
                $cashflow[0] = -round($this->leaseAmount * $this->leaseAmountRatio - $this->downPayment * $this->downPaymentRatio - $this->deposit * $this->depositRatio - $this->serviceAmount * $this->serviceAmountRatio, $this->scale);  // -(转让价款 - 首付款 - 预付款 - 手续费 - 其他费用)
            } else {
                if ($issue == $this->issues) {  // 修正最后一期的本金
                    $capital = $balance;
                } else {
                    $capital = round($front_plan['capital'] * $this->leaseAmountRatio, $this->scale);
                }
                $cashflow[$issue] = $capital + round($balance * $issueOutsideRate, $this->scale);
                $balance = round($balance - $capital, $this->scale);
            }
        }
        if ($this->paymentMode == 1) {  // 期初支付调整
            for ($i = 0; $i < $this->issues; $i++) {
                if ($i == 0) {
                    $cashflow[0] = $cashflow[0] + $cashflow[1];
                } else {
                    $cashflow[$i] = $cashflow[$i + 1];
                }
            }
            $cashflow[$this->issues] = 0.00;
        }

        // 预付款抵扣
        $deposit = round($this->deposit * $this->depositRatio, $this->issues);
        $start = $this->paymentMode == 1 ? $this->issues - 1 : $this->issues;
        for ($i = $start; $i > 0; $i--) {
            if ($deposit <= 0) {
                break;
            }
            if ($cashflow[$i] <= $deposit) {
                $deposit = round($deposit - $cashflow[$i], $this->scale);
                $cashflow[$i] = 0;
            } else {
                $cashflow[$i] = round($cashflow[$i] - $deposit, $this->scale);
                $deposit = 0;
            }
        }
        // 留购价
        if ($this->paymentMode == 1) {
            $cashflow[$this->issues] = round($this->residualValue * $this->residualValueRatio, $this->scale);
        } else {
            $cashflow[$this->issues] = round($cashflow[$this->issues] + $this->residualValue * $this->residualValueRatio, $this->scale);
        }
        // 其他费用
        if (!empty($this->otherfees2)) {
            foreach ($this->otherfees2 as $fee) {
                $cashflow[$fee['issue']] = round($cashflow[$fee['issue']] + $fee['amount'], $this->scale);
            }
        }
        return $cashflow;
    }
}